package com.pinfly.common.util;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * A class that can be used to describe a location in a Java application. A
 * location consists of a List of FileLocation objects. The list represents the
 * call stack of the program, and each entry in the list represents an item in
 * the call stack.
 * 
 * The list contains items of type FileLocation. Each FileLocation specifies a
 * class name, method name, source file name, line number, and source code
 * revision.
 * 
 * Static methods are provided that return the location of the caller of the
 * method, or the location associated with an exception.
 */
public class FileLocation
{
    private static final String UNKNOWN_NAME = "unknown";
    private static final int UNKNOWN_REVISION = -1;
    private static final int UNKNOWN_LINE_NUMBER = -1;

    private static final FileLocation UNKNOWN_LOCATION = new FileLocation (UNKNOWN_NAME, UNKNOWN_NAME, UNKNOWN_NAME,
                                                                           UNKNOWN_LINE_NUMBER, UNKNOWN_REVISION);

    private final String m_className;
    private final String m_methodName;
    private final String m_fileName;
    private final int m_lineNum;
    private final int m_revisionNum; // source code revision number

    /**
     * Creates a new instance.
     * 
     * @param className The name of the class.
     * @param methodName The name of the method.
     * @param fileName The name of the file.
     * @param lineNum The line number.
     * @param revision The source code revision number.
     */
    private FileLocation (String className, String methodName, String fileName, int lineNum, int revision)
    {
        m_className = className;
        m_methodName = methodName;
        m_fileName = fileName;
        m_lineNum = lineNum;
        m_revisionNum = revision;
    }

    /**
     * Create a new instance from the stack trace element.
     * 
     * @param elem The StackTraceElement to obtain the location information
     *            from.
     */
    private FileLocation (StackTraceElement elem)
    {
        String tmp = elem.getClassName ();
        m_className = tmp == null ? UNKNOWN_NAME : tmp;

        tmp = elem.getMethodName ();
        m_methodName = tmp == null ? UNKNOWN_NAME : tmp;

        tmp = elem.getFileName ();
        m_fileName = tmp == null ? UNKNOWN_NAME : tmp;

        m_lineNum = elem.getLineNumber ();
        m_revisionNum = internalGetRevisionNum (m_className);
    }

    /**
     * Get the class name of the location.
     * 
     * @return the fully qualified class name
     */
    public String getClassName ()
    {
        return m_className;
    }

    /**
     * Get the method name of the location.
     * 
     * @return the method name
     */
    public String getMethodName ()
    {
        return m_methodName;
    }

    /**
     * Get the source file name of the location.
     * 
     * @return the source file name
     */
    public String getFileName ()
    {
        return m_fileName;
    }

    /**
     * Get the line number of the location.
     * 
     * The line number will always be zero for native methods. The line number
     * may not be accurate for Java methods if the code has been optimized.
     * 
     * @return the line number
     */
    public int getLineNum ()
    {
        return m_lineNum;
    }

    /**
     * Get the source code revision number.
     * 
     * @return the revision number. For classes that don't contain the
     *         SCC_REVISION static field, -1 will be returned.
     */
    public int getRevisionNum ()
    {
        return m_revisionNum;
    }

    /**
     * Attempts to load the class, and obtain the VSS revision number from the
     * static public field SCC_REVISION. If any problems occur, or the class is
     * not a pinfly class, {@link #UNKNOWN_REVISION} is returned.
     * 
     * @param className The name of the class to obtain the source code revision
     *            number from.
     * @return The revision number.
     */
    private static int internalGetRevisionNum (String className)
    {
        int result = UNKNOWN_REVISION;

        // Only do this if we have a class name and
        // the class is in a Pinfly package
        if (className == null || !className.startsWith ("net.pinfly."))
        {
            return result;
        }

        try
        {
            Class c = Class.forName (className, true, Thread.currentThread ().getContextClassLoader ());
            Field f = c.getDeclaredField ("SCC_REVISION");
            String rev = (String) f.get (null);

            if (rev != null)
            {
                // Just get the revision number
                // The revision number is in the following
                // format:
                // "$" + "Revision:" + num + "$"
                if (rev.length () <= 10)
                {
                    // Wrong format
                    return result;
                }
                // Get rid of the tag, and trailing dollar sign
                rev = rev.substring (10, rev.length () - 1).trim ();
                result = Integer.parseInt (rev);
            }
        }
        catch (NoSuchFieldException e)
        {
            // Inner classes typically don't have the revision static
            // field. Try with the container class. Inner
            // classes always have '$' which separate them from their
            // container class.
            int index = className.lastIndexOf ('$');
            if (index > -1)
            {
                className = className.substring (0, index);
                return internalGetRevisionNum (className);
            }
        }
        catch (Throwable e)
        {
            // An error occured. Most likely a class loader issue.
            // Don't include the revision number.
        }

        return result;
    }

    /**
     * Get the location where the exception was generated.
     * 
     * Returns a list of FileLocations that describe where the exception was
     * generated. The first entry in the list is the line that caused the
     * exception. The second entry in the list is the line that called the
     * method that caused the exception. The third entry is the line that called
     * the method in the second entry, etc.
     * 
     * @param t The throwable whose location information is to be returned
     * @return A list of FileLocations that describe where the throwable was
     *         generated. The method is guaranteed to return at least one
     *         location (the size of the list will never be 0).
     * @throws IllegalArgumentException if t is null.
     */
    public static List getLocation (Throwable t)
    {
        if (t == null)
        {
            throw new IllegalArgumentException ("t can not be null!");
        }

        List locs = new ArrayList ();

        StackTraceElement[] stackElements = t.getStackTrace ();
        for (int i = 0; i < stackElements.length; i++)
        {
            StackTraceElement element = stackElements[i];
            locs.add (new FileLocation (element));
        }

        // The documentation on getStackTrace indicates that a JVM
        // could return no stack trace elements. We have components
        // in the system (like Logging) that won't function properly if
        // there are no stack elements. Return at least one element.
        if (locs.size () == 0)
        {
            locs.add (UNKNOWN_LOCATION);
        }
        return locs;
    }

    /**
     * Get the location that called this method.
     * 
     * Returns a list of FileLocations. The first item in the list has the
     * location of the call to this function. The second item has the location
     * of the caller to the first function, etc.
     * 
     * @return the list of FileLocations. The method is guaranteed to return at
     *         least one location (the size of the list will never be 0).
     */
    public static List getLocation ()
    {
        /*
         * Create a new exception, then call getLocation (Throwable) to
         * determine where it was created. Then discard the first line, since
         * that will be this function. What's left is who called this function.
         */
        List locs = getLocation (new Throwable ());
        locs.remove (0); // don't want entry for above line

        // It is possible that there are no locations now left in
        // the list. This could cause problems with callers who
        // expect at least one location to be returned.
        if (locs.size () == 0)
        {
            locs.add (UNKNOWN_LOCATION);
        }
        return locs;
    }

}
